/**
 * Copyright (c) <2010>, <SUNNY GUPTA>
 * All rights reserved.

 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the <ORGANIZATION> nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <SUNNY GUPTA> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.gwtp.investmentadvisor.server.externaldataservices.yahoo;

import java.io.IOException;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.google.gson.Gson;
import com.gwtp.investmentadvisor.server.core.communication.URLFetchService;
import com.gwtp.investmentadvisor.server.core.datastore.dao.FinancialSecurityDAO;
import com.gwtp.investmentadvisor.server.core.exception.NotImplementedException;
import com.gwtp.investmentadvisor.server.core.exception.NotSupportedException;
import com.gwtp.investmentadvisor.server.core.exception.NullArgumentException;
import com.gwtp.investmentadvisor.server.entities.finance.FinancialSecurity;
import com.gwtp.investmentadvisor.server.entities.finance.FinancialSecurityQuote;
import com.gwtp.investmentadvisor.server.entities.general.Country;
import com.gwtp.investmentadvisor.server.externaldataservices.FinancialSecurityDataProvider;
import com.gwtp.investmentadvisor.server.externaldataservices.HistoricalQuotePeriod;
import com.gwtp.investmentadvisor.server.externaldataservices.exception.DataServiceException;
import com.gwtp.investmentadvisor.shared.entities.FinancialSecurityView;
import com.gwtp.investmentadvisor.shared.utilities.Encoder;

/**
 * @author Sunny Gupta
 *
 */
public class YahooFinanceDataProvider implements FinancialSecurityDataProvider {
  private final String quotesCsvBaseUrl = "finance.yahoo.com/d/quotes.csv?s="; 
  private final String histQuotesCsvBaseUrl = "http://ichart.yahoo.com/table.csv?s=";

  // The client should specify the Query Method during instantiation
  // TODO: Implement YQL
  public YahooFinanceDataProvider(QueryMethodType queryMethod,
    FinancialSecurityDAO financialSecurityDAO) throws NullArgumentException {
    // Check for null
    if (queryMethod == null) {
      throw new NullArgumentException("queryMethod");
    }
    if (financialSecurityDAO == null) {
      throw new NullArgumentException("financialSecurityDAO");
    }
    
    this.setQueryMethod(queryMethod);
    this.financialSecurityDAO = financialSecurityDAO;
  }

  private QueryMethodType queryMethod;
  private final FinancialSecurityDAO financialSecurityDAO;
  
  /* (non-Javadoc)
   * @see com.gwt.investmentadvisor.server.dataservices.FinancialSecurityDataProvider#getData(com.gwt.investmentadvisor.shared.FinancialSecurity)
   */
  @Override
  public FinancialSecurityQuote getCurrentMarketPrice(FinancialSecurity financialSecurity) throws DataServiceException {
    try {
      // Build lists
      ArrayList<FinancialSecurity> securityList = new ArrayList<FinancialSecurity>();
      securityList.add(financialSecurity);
      ArrayList<QueryParameter> queryParamList = new ArrayList<QueryParameter>();
      queryParamList.add(QueryParameter.LastTradeWithTime);
      // The Current implementation ignores Query Method and uses CSV method
      String quotesData = downloadCSVQuotes(securityList,queryParamList);
      
      BigDecimal price = null;
      Date date = null;
      // TODO: Parse Data
      FinancialSecurityQuote quote = new FinancialSecurityQuote(financialSecurity.getKey(),
          price, date);
      
      return quote;
    } catch (Exception e) {
      throw new DataServiceException(e);
    }
  }

  /* (non-Javadoc)
   * @see com.gwt.investmentadvisor.server.dataservices.FinancialSecurityDataProvider#getHistoricalQuotes(com.gwt.investmentadvisor.server.entities.FinancialSecurity, java.util.Date, java.util.Date)
   */
  @Override
  public List<FinancialSecurityQuote> getHistoricalQuotes(FinancialSecurity financialSecurity,
      Calendar startDate, Calendar endDate, HistoricalQuotePeriod quotePeriod) throws DataServiceException {
    try {
      // The Current implementation ignores Query Method and uses CSV method
      String quotesData = downloadCSVHistoricalQuotes(financialSecurity,startDate,endDate,quotePeriod);
      // TODO: Parsing
      quotesData.length();
      return null;
    } catch (Exception e) {
      throw new DataServiceException(e);
    }
  }
  
  /**
   * This method assumes that all the securities belong to the same country.
   * It'll assume the country as the country of first security in the list
   * @param securityList
   * @return
   * @throws InvalidQueryMethodException 
   * @throws NotImplementedException 
   * @throws InvalidQueryException 
   * @throws IOException 
   * @throws MalformedURLException 
   */
  private String downloadCSVQuotes(List<FinancialSecurity> securityList, List<QueryParameter> queryParameters) 
      throws InvalidQueryException, NotImplementedException, InvalidQueryMethodException, MalformedURLException, IOException {
    // Check for null/ empty
    if (securityList == null || securityList.isEmpty()) {
      return "";
    }
    
    // Form the Query URL
    // Query Method
    StringBuilder url = new StringBuilder();
    // Get Server from country of first element
    url.append(getQueryMethodBaseURL(securityList.get(0),QueryType.QUOTE,QueryMethodType.CSV));
  
    // Add Symbol
    int listSize = securityList.size();
    int listSizeMinus1 = listSize - 1;
    for (int i = 0; i < listSize; i++) {
      url.append(securityList.get(i).getTickerSymbol().trim());
      if (i < listSizeMinus1) {
        url.append("+");
      }
    }
    
    // Form the Query
    url.append(getCSVQueryParametersString(queryParameters));
    
    // Use URLFetch
    String response = URLFetchService.urlGet(url.toString());
    
    // Done
    return response;
  }
  
  private String downloadCSVHistoricalQuotes(FinancialSecurity financialSecurity,
      Calendar startDate, Calendar endDate, HistoricalQuotePeriod quotePeriod) 
      throws MalformedURLException, IOException, InvalidQueryException, NotImplementedException, InvalidQueryMethodException {
    
    int startDay = startDate.get(Calendar.DAY_OF_MONTH);
    int startMonth = startDate.get(Calendar.MONTH);
    int startYear = startDate.get(Calendar.YEAR);
    int endDay = endDate.get(Calendar.DAY_OF_MONTH);
    int endMonth = endDate.get(Calendar.MONTH);
    int endYear = endDate.get(Calendar.YEAR);

    // Form the Query URL
    StringBuilder url = new StringBuilder();
    url.append(getQueryMethodBaseURL(financialSecurity,QueryType.HISTORICAL,QueryMethodType.CSV));
    String queryString = String.format(
        "%s&a=%d&b=%d&c=%d&d=%d&e=%d&f=%d&g=",
        financialSecurity.getTickerSymbol(),
        startMonth,
        startDay,
        startYear,
        endMonth,
        endDay,
        endYear);
    url.append(queryString);
    // Interval
    switch (quotePeriod) {
      case DAY: url.append("d"); break;
      case WEEK: url.append("w"); break;
      case MONTH: url.append("m"); break;
      default:
        break;
    }
    
    // Use URLFetch
    String response = URLFetchService.urlGet(url.toString());
    return response;
  }
  
  /**
   * @param queryParameters
   * @return
   */
  private String getCSVQueryParametersString(
      List<QueryParameter> queryParameters) {
    StringBuilder paramString = new StringBuilder();
    // Append separater and field
    paramString.append("&f=");
    // Add query parameters
    for (QueryParameter queryParam : queryParameters) {
      paramString.append(queryParam.getQueryString());
    }
    // Append file extension
    paramString.append("&e=.csv");
    return paramString.toString();
  }

  /**
   * @param financialSecurity
   * @param quote
   * @return
   * @throws InvalidQueryException 
   * @throws NotImplementedException 
   * @throws InvalidQueryMethodException 
   */
  private String getQueryMethodBaseURL(FinancialSecurity financialSecurity,
      QueryType queryType, QueryMethodType queryMethod) throws InvalidQueryException, NotImplementedException, InvalidQueryMethodException {
    StringBuilder url = new StringBuilder();
    // First get URL based on QueryType
    switch(queryType) {
    case QUOTE:
      // Now get URL based on QueryMethod
      switch(queryMethod) {
      case CSV:
        // Get Server based on country
        url.append("http://");
        url.append(getURLServerPrefix(financialSecurityDAO.getCountry(financialSecurity)));
        // Add Base URL
        url.append(quotesCsvBaseUrl);
        // Done, return
        return url.toString();
      case YQL:
        throw new NotImplementedException();
      default:
        throw new InvalidQueryMethodException();
      }
    case HISTORICAL:
      // Now get URL based on QueryMethod
      switch(queryMethod) {
      case CSV:
        return histQuotesCsvBaseUrl;
      case YQL:
        throw new NotImplementedException();
      default:
        throw new InvalidQueryMethodException();
      }
    default: throw new InvalidQueryException();
    }
  }

  /**
   * @param country
   * @return
   * @throws NotImplementedException 
   */
  private String getURLServerPrefix(Country country) throws NotImplementedException {
    String prefix = country.getCountryID().toLowerCase();
    // Only India is implemented now
    // Can also work simply as prefix + "."
    if ("in".equals(prefix)) {
      return prefix + ".";
    } else {
      throw new NotImplementedException();
    }
  }

  /**
   * @param queryMethod the queryMethod to set
   */
  public void setQueryMethod(QueryMethodType queryMethod) {
    this.queryMethod = queryMethod;
  }

  /**
   * @return the queryMethod
   */
  public QueryMethodType getQueryMethod() {
    return queryMethod;
  }

  /* (non-Javadoc)
   * @see com.gwt.investmentadvisor.server.dataservices.FinancialSecurityDataProvider#searchByText(java.lang.String)
   */
  @Override
  public List<FinancialSecurityView> searchByText(String searchText) throws DataServiceException {
    try {
      String result = searchJSONID(searchText);
      // Deserialize JSON & Convert to List<FinancialSecurityView>
      return parseJSONSearchDataToFinancialSecurityView(result);
    } catch (Exception e) {
      throw new DataServiceException(e);
    }
  }
  
  /**
   * @param result
   * @return
   */
  private List<FinancialSecurityView> parseJSONSearchDataToFinancialSecurityView(
      String result) {
    // Extract JSON string for search results
    Pattern pattern = Pattern.compile("\\{\"symbol\":.*?\\}");
    Matcher matcher = pattern.matcher(result);
    Gson gson = new Gson();
    
    ArrayList<FinancialSecurityView> objectList = new ArrayList<FinancialSecurityView>(); 
    while (matcher.find()) {    
      FinancialSecurityView obj = gson.fromJson(matcher.group(), JSONSearchResult.class).getFinancialSecurityView();
      objectList.add(obj);
    }
    return objectList;
  }

  /**
   * Text Search for Symbol IDs.
   * Returns a JSON formatted string which can be parsed using GWT JSON support
   */
  private static final String JSON_ID_SEARCH_URL_START = "http://d.yimg.com/autoc.finance.yahoo.com/autoc?query=";
  private static final String JSON_ID_SEARCH_URL_END = "&callback=YAHOO.Finance.SymbolSuggest.ssCallback";
  private String searchJSONID(String searchText) throws MalformedURLException, IOException {
    StringBuilder url = new StringBuilder();
    url.append(JSON_ID_SEARCH_URL_START);
    
    // Encode search string
    url.append(Encoder.encodeUrlString(searchText));
    
    url.append(JSON_ID_SEARCH_URL_END);
    
    // Use URLFetch
    String response = URLFetchService.urlGet(url.toString());
    return response;
  }

  @Override
  public List<FinancialSecurityQuote> getCurrentMarketPriceForAllFinancialSecurities()
      throws DataServiceException, NotSupportedException {
    throw new NotSupportedException("Yahoo Financial Data Provider does not support this " +
        "operation, since it'd result in all of the world's possible Stocks being returned." +
        " This would certainly be useless for the client, needless to mention the huge " +
        "amount of time this operation would take");
  }

  @Override
  public FinancialSecurityView getCurrentMarketPrice(
      FinancialSecurityView financialSecurityView)
      throws DataServiceException {
    try {
      throw new NotImplementedException();
    } catch (Exception e) {
      throw new DataServiceException(e);
    }
  }
}
